﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Bettzueche.RLLibrary.Component;
using Bettzueche.RLLibrary.Functions;
using Bettzueche.RLLibrary.Types;
using Bettzueche.RLLibrary.Util;

namespace RLProject1.Model.AMEEWorld
{
    /// <summary>
    /// Implementierung eines Sarsa(λ) Agenten mit ersetzenden Traces, Funktionsapproximation per Linear-Gradienten-Abstiegsmethode.<para>
    /// Es müssen minimal die Eigenschaften Environment, QFunktion, Policy gesetzt werden.</para><para>
    /// Wenn nicht anders gesetzt, wird <see cref="SarsaLambda{S,A}.DefaultEFunc"/> als EFunction genutzt.</para>
    /// </summary>
    /// <remarks>
    /// Diese Implementierung nutzt TraceSet, um (s,a) Tupel einer Episode zu speichern und so Updates effizient umzusetzen.<para>
    /// Wesentlicher Update-Schritt in DoStep: Q(s,a) += α·δ·e(s,a)</para><para>
    /// Aktuell funktioniert das nur für WorldImage mit EINEM Tiling!</para>
    /// </remarks>
    public class AMEEAgent : SarsaLambda<WorldImage, AMEEAction>
    {
        #region Standard RL parameters:
        /// <summary>probability of random action = 0.1</summary>
        public const double EPSILON = 0.1;
        /// <summary>step size parameter = 0.5</summary>
        public const double ALPHA = 0.5;
        /// <summary>trace-decay parameter = 0.9</summary>
        public const double LAMBDA = 0.9;
        /// <summary>discount-rate parameters = 1.0</summary>
        public const double GAMMA = 1.0;
        #endregion


        /// <summary>
        /// Gets or sets the Q function.
        /// </summary>
        /// <value>
        /// The Q function.
        /// </value>
        public new QFunktion QFunction {
            get { return _qFunc; }
            set {
                base.QFunction = value;
                _qFunc = value;
            }
        }
        QFunktion _qFunc;

        #region Konstruktor(en)

        ///// <summary>
        ///// Initializes a new instance of the <see cref="AMEEAgent"/> class.<para>
        ///// Damit der Algorithmus funktioniert , müssen Environment, QFunktion und Policy gesetzt werden!
        ///// Für EFunction wird eine Standardimplementierung verwendet, falls nicht anders gesetzt wird.</para>
        ///// </summary>
        //internal AMEEAgent() : base() {
        //}

        /// <summary>
        /// Initializes a new instance of the <see cref="AMEEAgent"/> class.
        /// </summary>
        /// <param name="weltKomponente">The welt komponente.</param>
        /// <param name="qFunc">The q func.</param>
        /// <param name="policy">The policy.</param>
        /// <exception cref="System.ArgumentNullException">
        /// weltKomponente
        /// or
        /// qFunc
        /// or
        /// policy
        /// </exception>
        public AMEEAgent(IEnvironment<WorldImage, AMEEAction, double> weltKomponente, QFunktion qFunc, GenericExplorationPolicy<AMEEAction> policy)
            : base(weltKomponente, qFunc, policy)
        {
            if (weltKomponente == null)
                throw new ArgumentNullException("weltKomponente");
            if (qFunc == null)
                throw new ArgumentNullException("qFunc");
            if (policy == null)
                throw new ArgumentNullException("policy");
            this.Environment = weltKomponente;
            this.QFunction = qFunc;
            this.Policy = policy;
        }

        #endregion

        /// <summary>
        /// Überprüft, ob QFunction, Environment und Policy gesetzt sind.<para>
        /// Falls Policy == null, wird EpsilonGreedy{AMEEAction}(AgentAMEE.EPSILON) gesetzt.</para>
        /// </summary>
        /// <param name="initialState">Wird ignoriert. Zustand wird intern stattdessen von Environment erfragt.</param>
        /// <param name="specifications">wird ignoriert</param>
        /// <exception cref="System.RLProjectException">QFunction oder Policy nicht gesetzt.</exception>
        public override void Init(WorldImage initialState, string specifications) {
            //if (initialState == null) {
            //    throw new ArgumentNullException("initialState");
            //}
            //zustand = initialState;
            ////nonzero_traces = new BesuchteZustaende(); // initial capacity?
            //AblaufVerfolgung = new TraceQueue<SATupel<WorldImage, AMEEAction>>();           // NEU!!!
            ////eligibilities = new Dictionary<int, double>(); // initial capacity?
            //eligibilities = new Dictionary<SATupel<WorldImage, AMEEAction>, double>();

            if (QFunction == null)
                //QFunction = new QFunktion(...)
                throw new RLProjectException("Eigenschaft 'QFunction' muss gesetzt sein.");
            if (Environment == null)
                throw new RLProjectException("Eigenschaft 'Environment' muss gesetzt sein.");
            if (Policy == null)
                Policy = new EpsilonGreedy<AMEEAction>(AMEEAgent.EPSILON);
                //throw new RLProjectException("Eigenschaft 'Policy' muss gesetzt sein.");
        }

        /// <summary>
        /// Does the step.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <returns>next action</returns>
        public override AMEEAction DoStep(AMEEAction action) {
            // Workaround für die generisch erzeugten Actions in EpsilonGreedy<A>:
            if (action.X == 0.0 && action.Y == 0.0) {
                AMEEAction act = Environment.State.GetAction(action.Abschnittsnummer);
                return base.DoStep(act);
            }
            else
                return base.DoStep(action);
        }
    }



    /// <summary>
    /// Linearer Gradienten-Abstiegs Sarsa-Lamda Beispiel Agent.<para>
    /// </para><para>
    ///  A. Initialisiere theta beliebig
    ///  B. Wiederhole (für jede Episode):
    ///    1. v_e = 0
    ///    2. s,a = initialer Zustand,Aktion der Episode
    ///    3. Fa = Menge von Merkmalen die zu s,a gehören (Indexmenge)
    ///    4. Wiederhole für jeden Schritt der Episode:
    ///      1) Für alle i in Fa:
    ///        a) v_e_i += 1 (akkumulierende Traces)
    ///   oder b) v_e_i = 1 (ersätzende Traces)
    ///    5. Führe Aktion a aus, beobachte Belohnung r und Folgezustand s
    ///    6. delta = r - Sum(theta_i) | i in Fa
    ///    7. Mit W.keit 1-epsilon:
    ///    7.1) Für alle in s möglichen a:
    ///           Fa = Merkmalsmenge von s,a
    ///           Qa = Sum(theta_i) | i in Fa
    ///         a = argMax_a(Qa)
    ///sonst 2) wähle a zufällig aus A(s)
    ///         Fa = Merkmalsmenge von s,a
    ///         Qa = Sum(theta_i) | i in Fa
    ///    8. delta += gamma*Qa
    ///    9. theta += alpha * delta * v_e
    ///   10. v_e *= gamma * lambda
    ///  bis s Endzustand
    /// </para>
    /// </summary>
    //class AMEEAgentErsterEntwurf : GenericAgent<WorldImage,AMEEAction>
    //{
    //    #region Standard RL parameters:
    //    /// <summary>probability of random action = 0</summary>
    //    public const double EPSILON = 0.0;
    //    /// <summary>step size parameter = 0.5</summary>
    //    public const double ALPHA = 0.5;
    //    /// <summary>trace-decay parameter = 0.9</summary>
    //    public const double LAMBDA = 0.9;
    //    /// <summary>discount-rate parameters = 1.0</summary>
    //    public const double GAMMA = 1.0;
    //    #endregion

    //    #region private Fields

    //    /// <summary>(Start- / aktueller) Zustand</summary>
    //    WorldImage zustand;
    //    ///// <summary>e-Werte (größer Null?). TODO: ggf. Liste oder so verwenden. Sutton nutzt Indexliste für Werte gr 0.</summary>
    //    //Dictionary<int, double> eligibilities;
    //    /// <summary>Minimalwert 0.01 für e-Trace.</summary>
    //    double minimumTrace = 0.01;
    //    ///// <summary>ID-Liste zu 'besuchten Zuständen' (oder durchlaufene s,a-Tupel).</summary>
    //    //BesuchteZustaende nonzero_traces;
    //    /// <summary>Hält die e-Wert (value) zu einem SA-Tupel (key).</summary>
    //    Dictionary<SATupel<WorldImage, AMEEAction>, double> eligibilities;

    //    /// <summary>
    //    /// 'besuchte Zustände' (oder durchlaufene s,a-Tupel).
    //    /// </summary>
    //    /// <value>
    //    /// Die Pfadliste (<see cref="Bettzueche.RLLibrary.Util.TraceQueue"/>) zum speichern der besuchten s,a Tupel
    //    /// </value>
    //    /// <remarks>Speichert die Reihenfolge der SA-Tupel, welche auch die keys zu eligibilities sind.</remarks>
    //    public TraceQueue<SATupel<WorldImage, AMEEAction>> AblaufVerfolgung { get; protected set; }


    //    #endregion

    //    #region Properties

    //    /// <summary>
    //    /// Gets or sets the policy.
    //    /// </summary>
    //    /// <value>
    //    /// The policy.
    //    /// </value>
    //    /// <exception cref="System.NotImplementedException">
    //    /// </exception>
    //    public override GenericExplorationPolicy<AMEEAction> Policy {
    //        get;
    //        set;
    //    }

    //    /// <summary>
    //    /// Gets or sets the environment.
    //    /// </summary>
    //    /// <value>
    //    /// The environment.
    //    /// </value>
    //    public override IEnvironment<WorldImage, AMEEAction, double> Environment {
    //        get;
    //        set;
    //    }

    //    /// <summary>
    //    /// Not Implemented.
    //    /// </summary>
    //    /// <value>Not Implemented.</value>
    //    /// <exception cref="System.NotImplementedException">
    //    /// </exception>
    //    public override IValueFunction<WorldImage> VFunction {
    //        get {
    //            throw new NotImplementedException();
    //        }
    //        set {
    //            throw new NotImplementedException();
    //        }
    //    }

    //    /// <summary>
    //    /// Äqivalent zur Eigenschaft QFunctionAMEE.
    //    /// </summary>
    //    /// <value>
    //    /// The Q function vom Typ <see cref="QFunktion"/>.
    //    /// </value>
    //    /// <seealso cref="RLProject1.Model.AMEEWorld.QFunktion"/>
    //    /// <exception cref="System.NotSupportedException">Nur Funktionen vom Typ QFunktion.</exception>
    //    public override IStateValueFunction<WorldImage, AMEEAction> QFunction {
    //        get { return this.QFunctionAMEE; }
    //        set {
    //            if (value is QFunktion)
    //                this.QFunctionAMEE = (QFunktion)value;
    //            else
    //                throw new NotSupportedException("Nur Funktionen vom Typ QFunktion.");
    //        }
    //    }
    //    /// <summary>
    //    /// Gets or sets the Q function.
    //    /// </summary>
    //    /// <value>
    //    /// The Q function.
    //    /// </value>
    //    public QFunktion QFunctionAMEE {
    //        get;
    //        set;
    //    }

    //    /// <summary>
    //    /// Not Implemented
    //    /// </summary>
    //    /// <value>
    //    /// Not Implemented
    //    /// </value>
    //    /// <exception cref="System.NotImplementedException">
    //    /// </exception>
    //    public override IEligibilityFunction<WorldImage, AMEEAction> EFunction {
    //        get {
    //            throw new NotImplementedException();
    //        }
    //        set {
    //            throw new NotImplementedException();
    //        }
    //    }

    //    /// <summary>
    //    /// Not implemented.
    //    /// </summary>
    //    /// <value>
    //    /// Not implemented.
    //    /// </value>
    //    /// <exception cref="System.NotImplementedException">
    //    /// </exception>
    //    public override StochasticPolicy Pi {
    //        get {
    //            throw new NotImplementedException();
    //        }
    //        set {
    //            throw new NotImplementedException();
    //        }
    //    }

    //    /// <summary>
    //    /// Gets or sets the learn rate ('alpha').
    //    /// <para>(Optional)</para>
    //    /// </summary>
    //    /// <value>
    //    /// The learn rate.
    //    /// </value>
    //    public override double LearnRate {
    //        get;
    //        set;
    //    }

    //    /// <summary>
    //    /// Gets or sets the discount factor ('gamma').
    //    /// </summary>
    //    /// <value>
    //    /// The discount factor.
    //    /// </value>
    //    public override double DiscountFactor {
    //        get;
    //        set;
    //    }

    //    /// <summary>
    //    /// Gets or sets the trace decay ('lambda').
    //    /// <para>(Optional)</para>
    //    /// </summary>
    //    /// <value>
    //    /// The trace decay. Should be in [0.0, 1.0].
    //    /// </value>
    //    public override double TraceDecay {
    //        get;
    //        set;
    //    }

    //    #endregion


    //    #region Konstruktor
    //    // Default
    //    #endregion


    //    #region public Methods

    //    /// <summary>
    //    /// Inits the specified initial state.
    //    /// </summary>
    //    /// <param name="initialState">The initial state.</param>
    //    /// <param name="specifications">wird ignoriert</param>
    //    /// <exception cref="System.NotImplementedException"></exception>
    //    /// <exception cref="System.ArgumentNullException">initialState</exception>
    //    public override void Init(WorldImage initialState, string specifications) {
    //        if (initialState == null) {
    //            throw new ArgumentNullException("initialState");
    //        }
    //        zustand = initialState;
    //        //nonzero_traces = new BesuchteZustaende(); // initial capacity?
    //        AblaufVerfolgung = new TraceQueue<SATupel<WorldImage, AMEEAction>>();           // NEU!!!
    //        //eligibilities = new Dictionary<int, double>(); // initial capacity?
    //        eligibilities = new Dictionary<SATupel<WorldImage, AMEEAction>, double>();

    //        if (QFunctionAMEE == null)
    //            //QFunctionAMEE = new QFunktion(...)
    //            throw new RLProjectException("Eigenschaft 'QFunctionAMEE' muss gesetzt sein.");
    //        if (Environment == null)
    //            throw new RLProjectException("Eigenschaft 'Environment' muss gesetzt sein.");
    //        if (Policy == null)
    //            throw new RLProjectException("Eigenschaft 'Policy' muss gesetzt sein.");
    //        
    //    }

    //    /// <summary>
    //    /// Führt einzelnen Schritt (einer Episode) und eine Strategieverbesserung aus.
    //    /// </summary>
    //    /// <param name="action">Auszuführende Aktion</param>
    //    /// <returns>nächste auszuführende Aktion gemäß der angewandten Strategie.</returns>
    //    /// <exception cref="System.NotImplementedException"></exception>
    //    public override AMEEAction DoStep(AMEEAction action) {
    //        WorldImage state = Environment.State;
    //        WorldImage nextState;
    //        var sa = new SATupel<WorldImage, AMEEAction>(state, action);
    //        double reward = Environment.DoAction(action, out nextState);
    //        AblaufVerfolgung.Add(sa);

    //        AMEEAction nextAction = Policy.GetAction(this.QFunction.GetEstimates(nextState));

    //        double delta = reward + this.DiscountFactor * QFunction[nextState, nextAction] - QFunction[state, action];

    //        //neueste sa-Tupel zum Trace hinzufügen:
    //        if (eligibilities.ContainsKey(sa)) { // Note: O(1) auf Dictionary. O(n) auf TraceQueue!
    //            AblaufVerfolgung.Remove(sa); // ggf alten Eintrag entfernen.
    //        }
            

    //        // update Qs:
    //        this.updateQ(delta); // erst alte theta's anpassen

    //        this.zustand = nextState; // wozu? kann ggf. weg
    //        return nextAction;
    //    }

    //    /// <summary>
    //    /// Q bzw. Theta Werte und e-Werte für besuchte Zustände aktualisieren (backtrace).<para>
    //    /// Q(s,a) += alpha * delta * e(s,a);
    //    /// e(s,a) *= gamma * lambda</para><para>
    //    /// Ersetzende Traces: d.h., der erste=neueste Eintrag in Ablaufverfolgung erhält e=1</para>
    //    /// </summary>
    //    /// <param name="delta">The delta.</param>
    //    void updateQ(double delta) {
    //        double decay_rate = this.DiscountFactor * this.TraceDecay; // γ·λ
    //        double qUpdateFactor = this.LearnRate * delta; // α·δ

    //        // Sonderfall Erster/Neuester Eintrag mit ersetzenden Traces:
    //        var sa = AblaufVerfolgung[0];
    //        eligibilities[sa] = 1.0;
    //        QFunctionAMEE[sa.Item1, sa.Item2] = qUpdateFactor;

    //        // Aus Performancegründen:
    //        bool truncating = false;
    //        int truncateIdx = AblaufVerfolgung.Count; //nonzero_traces.Count;

    //        // über alle 'alten' s,a-Tupel 'zeitlich' zurück:
    //        for (int i = 1; i < AblaufVerfolgung.Count; i++) {
    //            //int featureId = nonzero_traces[i];
    //            sa = AblaufVerfolgung[i];
    //            if (truncating) {
    //                eligibilities.Remove(sa);
    //            }
    //            else {
    //                double e_i = eligibilities[sa] * decay_rate;
    //                if (e_i < this.minimumTrace) {
    //                    // ab jetzt keine Updates mehr, übrige 'Zustände' sind aus Liste zu entfernen
    //                    eligibilities.Remove(sa);
    //                    truncating = true;
    //                    truncateIdx = i;
    //                }
    //                else { 
    //                    // Update e und theta
    //                    eligibilities[sa] = e_i;
    //                    QFunctionAMEE[sa.Item1, sa.Item2] += qUpdateFactor * e_i;
    //                }
    //            }
    //        }
    //        if (truncating) {
    //            AblaufVerfolgung.Truncate(truncateIdx);
    //        }
    //    }
        

    //    /// <summary>
    //    /// Runs the episode.
    //    /// </summary>
    //    /// <param name="maxActionCount">The max action count.</param>
    //    /// <returns></returns>
    //    /// <exception cref="System.NotImplementedException"></exception>
    //    public override int RunEpisode(int maxActionCount) {
    //        throw new NotImplementedException();
    //    }

    //    /// <summary>
    //    /// Runs the episode.
    //    /// </summary>
    //    /// <param name="finalStates">The final states.</param>
    //    /// <returns></returns>
    //    /// <exception cref="System.NotImplementedException"></exception>
    //    public override int RunEpisode(params WorldImage[] finalStates) {
    //        throw new NotImplementedException();
    //    }

    //    /// <summary>
    //    /// Runs the episode.
    //    /// </summary>
    //    /// <param name="maxActionCount">The max action count.</param>
    //    /// <param name="finalStates">The final states.</param>
    //    /// <returns></returns>
    //    /// <exception cref="System.NotImplementedException"></exception>
    //    public override int RunEpisode(int maxActionCount, params WorldImage[] finalStates) {
    //        throw new NotImplementedException();
    //    }

        

    //    /// <summary>
    //    /// Resets this instance.
    //    /// </summary>
    //    /// <exception cref="System.NotImplementedException"></exception>
    //    public override void Reset() {
    //        throw new NotImplementedException();
    //    }

    //    #endregion





    //    /// <summary>
    //    /// Private Implementierung der E-Funktion. Wrapper für eine Dictionary
    //    /// </summary>
    //    class EFuncAMEE : IEligibilityFunction<WorldImage,AMEEAction>
    //    {
    //        #region private Felder

    //        /// <summary>Hält die e-Wert (value) zu einem SA-Tupel (key).</summary>
    //        Dictionary<SATupel<WorldImage, AMEEAction>, double> _eWerte;

    //        #endregion


    //        #region Konstruktor(en)

    //    /// <summary>
    //        /// Initializes a new instance of the <see cref="EFuncAMEE"/> class.
    //    /// </summary>
    //        /// <param name="minEligibility">Minimaler Wert für e. Sinkt ein e-Wert unter diese Schwelle, wird er als 0 interpretiert.</param>
    //        public EFuncAMEE() {
    //            _eWerte = new Dictionary<SATupel<WorldImage, AMEEAction>, double>();
    //        }

    //        #endregion


    //        #region IEligibilityFunction<WorldImage,AMEEAction> Members

    //    /// <summary>
    //        /// Gets or sets the <see cref="System.Double"/> eligibility value with the specified state, action.
    //    /// </summary>
    //        /// <value>
    //        /// Der Wert des Berechtungsfaktors e für das spezifizierte sa-Tupel. Sollte in [0.0, 1.0] liegen.
    //        /// </value>
    //        /// <param name="state">Zustand</param>
    //        /// <param name="action">Aktion</param>
    //        /// <returns>Den Berechtigungsfaktor e für das angegebene Tupel (state,action). Standardwert ist 0.0</returns>
    //        public double this[WorldImage state, AMEEAction action] {
    //            get {
    //                double retVal = 0.0;
    //                try {
    //                    retVal = _eWerte[new SATupel<WorldImage, AMEEAction>(state, action)];
    //    }
    //                catch (Exception) { /*nix*/
    //                    return retVal;
    //                }
    //                return retVal;
    //                    }
    //            set {
    //                var key = new SATupel<WorldImage, AMEEAction>(state, action);
    //                _eWerte[key] = value; // remove, wenn value == 0 !??
    //                }
    //    }

    //        #endregion


    //        #region IEligibilityFunction Members

    //    /// <summary>
    //        /// Löscht alle Einträge, bzw. setzt sie auf einen Standardwert zurück.
    //    /// </summary>
    //        public void Clear() {
    //            _eWerte.Clear();
    //    }

    //    /// <summary>
    //        /// Gets or sets the <see cref="System.Double"/> eligibility value with the specified state, action.<para>
    //        /// Implementierung von <see cref="Bettzueche.RLLibrary.Functions.IEligibilityFunction"/>.</para>
    //    /// </summary>
    //        /// <value>
    //        /// Der Wert des Berechtungsfaktors e. Sollte in [0.0, 1.0] liegen.
    //        /// </value>
    //        /// <param name="state">Zustand</param>
    //        /// <param name="action">Aktion</param>
    //        /// <returns>Den Berechtigungsfaktor e für das angegebene Tupel (state,action). Standardwert ist 0.0</returns>
    //        /// <exception cref="System.NotSupportedException">
    //        /// Mindest ein Agrument-Typ wird nicht unterstützt: state is WorldImage, action is AMEEAction ?
    //        /// </exception>
    //        public double this[IState state, IAction action] {
    //            get {
    //                WorldImage s = state as WorldImage;
    //                if (s != null) {
    //                    if (action is AMEEAction) {
    //                        var a = (AMEEAction)action;
    //                        return this[s, a];
    //                    }
    //                }
    //                throw new NotSupportedException("Mindest ein Agrument-Typ wird nicht unterstützt.");
    //            }
    //            set {
    //                WorldImage s = state as WorldImage;
    //                if (s != null) {
    //                    if (action is AMEEAction) {
    //                        var a = (AMEEAction)action;
    //                        this[s, a] = value;
    //        }
    //    }
    //                throw new NotSupportedException("Mindest ein Agrument-Typ wird nicht unterstützt.");
    //            }
    //        }

    //        #endregion
    //    }                                                
    //}
}
